package ua.edu.nuos.tees.jdmmon.dao.implementations;

import jssc.SerialPort;
import jssc.SerialPortException;
import jssc.SerialPortList;
import ua.edu.nuos.tees.jdmmon.dao.exceptions.CRCException;
import ua.edu.nuos.tees.jdmmon.dao.exceptions.ConfigException;
import ua.edu.nuos.tees.jdmmon.dao.interfaces.Dao;
import ua.edu.nuos.tees.jdmmon.vo.implementations.RtuInputPackage;
import ua.edu.nuos.tees.jdmmon.vo.implementations.RtuOutputPackage;
import ua.edu.nuos.tees.jdmmon.vo.interfaces.InputPackage;
import ua.edu.nuos.tees.jdmmon.vo.interfaces.OutputPackage;
import utils.ConfigurationLoader;
import vo.Config;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * Dao implementation for rs-232 serial port
 *
 * @author Timur Yarosh
 * @author Olha Mudrievska
 *         21.02.13
 */
public class Rs232Dao implements Dao {
    private final int DATA_SIZE;
    private final int OUTPUT_PACKAGE_DATA_SIZE;

    private final int MODBUS_READ_INPUT_REGISTER_FUNCTION;
    /*private final int MODBUS_PRESET_SINGLE_REGISTER_FUNCTION;
    private final int MODBUS_READ_EXCEPTION_FUNCTION;
    private final int MODBUS_PRESET_MULTIPLE_REGISTER_FUNCTION;
    private final int MODBUS_REPORT_SLAVE_ID_FUNCTION;*/
    private final SerialPort SERIAL_PORT;
    private final String PORT_NAME;
    private int deviceAddress; // = 1?
    private int baudRate;
    private int dataBits;
    private int stopBits;
    private int parity;

    /**
     * Initialize PORT_NAME, baudRate, dataBits, stopBits and parity.
     * Also initialize register addresses from device config values
     *
     * @throws ua.edu.nuos.tees.jdmmon.dao.exceptions.ConfigException
     *          if an error occurred while reading the config
     * @deprecated
     */

    public Rs232Dao(InputStream modbusConfigInputStream)
            throws ConfigException, SerialPortException, FileNotFoundException, exceptions.ConfigException {
        Properties rs232Settings = ConfigurationLoader.loadConfig(getClass().getResourceAsStream("/rs232_properties.ini")); // TODO set path
        String portName = rs232Settings.getProperty("portName");
        this.baudRate = Integer.decode(rs232Settings.getProperty("baudRate"));
        this.dataBits = Integer.decode(rs232Settings.getProperty("dataBits"));
        this.stopBits = Integer.decode(rs232Settings.getProperty("stopBits"));
        this.parity = Integer.decode(rs232Settings.getProperty("parity"));
        OUTPUT_PACKAGE_DATA_SIZE = 48;
        PORT_NAME = portName;
        Properties modbusConfig = ConfigurationLoader.loadConfig(modbusConfigInputStream);
        DATA_SIZE = Integer.decode(modbusConfig.getProperty("DATA_SIZE"));
        MODBUS_READ_INPUT_REGISTER_FUNCTION =
                Integer.decode(modbusConfig.getProperty("MODBUS_READ_INPUT_REGISTER_FUNCTION"));
        /*MODBUS_PRESET_SINGLE_REGISTER_FUNCTION =
                Integer.decode(modbusConfig.getProperty("MODBUS_PRESET_SINGLE_REGISTER_FUNCTION"));
        MODBUS_READ_EXCEPTION_FUNCTION =
                Integer.decode(modbusConfig.getProperty("MODBUS_READ_EXCEPTION_FUNCTION"));
        MODBUS_PRESET_MULTIPLE_REGISTER_FUNCTION =
                Integer.decode(modbusConfig.getProperty("MODBUS_PRESET_MULTIPLE_REGISTER_FUNCTION"));
        MODBUS_REPORT_SLAVE_ID_FUNCTION =
                Integer.decode(modbusConfig.getProperty("MODBUS_REPORT_SLAVE_ID_FUNCTION")); */
        SERIAL_PORT = new SerialPort(portName);
        SERIAL_PORT.openPort();
    }

    public Rs232Dao(Config rs232Config, Config modbusConfig) throws SerialPortException {
        String portName = rs232Config.getString("portName");
        this.baudRate = rs232Config.getInteger("baudRate");
        this.dataBits = rs232Config.getInteger("dataBits");
        this.stopBits = rs232Config.getInteger("stopBits");
        this.parity = rs232Config.getInteger("parity");
        this.deviceAddress = modbusConfig.getInteger("DEVICE_ADDRESS");    //TODO delete
        OUTPUT_PACKAGE_DATA_SIZE = 48;
        PORT_NAME = portName;
        DATA_SIZE = modbusConfig.getInteger("DATA_SIZE");
        MODBUS_READ_INPUT_REGISTER_FUNCTION = modbusConfig.getInteger("MODBUS_READ_INPUT_REGISTER_FUNCTION");
        /*MODBUS_PRESET_SINGLE_REGISTER_FUNCTION = modbusConfig.getInteger("MODBUS_PRESET_SINGLE_REGISTER_FUNCTION");
        MODBUS_READ_EXCEPTION_FUNCTION = modbusConfig.getInteger("MODBUS_READ_EXCEPTION_FUNCTION");
        MODBUS_PRESET_MULTIPLE_REGISTER_FUNCTION = modbusConfig.getInteger("MODBUS_PRESET_MULTIPLE_REGISTER_FUNCTION");
        MODBUS_REPORT_SLAVE_ID_FUNCTION = modbusConfig.getInteger("MODBUS_REPORT_SLAVE_ID_FUNCTION");*/
        SERIAL_PORT = new SerialPort(portName);
        SERIAL_PORT.openPort();
    }

    /**
     * Get names of available com ports
     *
     * @return string array with names of available com ports
     */
    public static String[] getComPortNames() {
        return SerialPortList.getPortNames();
    }

    /**
     * Set device address
     *
     * @param deviceAddress device address
     */
    public void setDeviceAddress(int deviceAddress) {
        this.deviceAddress = deviceAddress;
    }

    /**
     * Get device address
     *
     * @return device address
     */
    public int getDeviceAddress() {
        return deviceAddress;
    }

    /**
     * Get data transfer rate
     *
     * @return data transfer rate
     */
    public int getBaudRate() {
        return baudRate;
    }

    /**
     * Set data transfer rate
     *
     * @param baudRate data transfer rate
     */
    public void setBaudRate(int baudRate) {
        this.baudRate = baudRate;
    }

    /**
     * Get data bits number
     *
     * @return data bits number
     */
    public int getDataBits() {
        return dataBits;
    }

    /**
     * Set data bits number
     *
     * @param dataBits data bits number
     */
    public void setDataBits(int dataBits) {
        this.dataBits = dataBits;
    }

    /**
     * Get stop bits number
     *
     * @return stop bits number
     */
    public int getStopBits() {
        return stopBits;
    }

    /**
     * Set stop bits number
     *
     * @param stopBits stop bits number
     */
    public void setStopBits(int stopBits) {
        this.stopBits = stopBits;
    }

    /**
     * Get parity
     *
     * @return parity
     */
    public int getParity() {
        return parity;
    }

    /**
     * Set parity
     *
     * @param parity parity
     */
    public void setParity(int parity) {
        this.parity = parity;
    }

    /**
     * Write output package to port
     *
     * @param outputPackage output package
     * @throws SerialPortException if an error occurred while writing data
     */
    protected void writePackage(OutputPackage outputPackage) throws SerialPortException {
        writeBytes(outputPackage.toBytes());
    }

    /**
     * Write byte array to port
     *
     * @param data byte array
     * @throws SerialPortException if an error occurred while writing data
     */
    private void writeBytes(byte[] data) throws SerialPortException {
        if (!SERIAL_PORT.isOpened()) SERIAL_PORT.openPort();
        SERIAL_PORT.setParams(baudRate, dataBits, stopBits, parity);
        SERIAL_PORT.writeBytes(data);
    }

    /**
     * Read byte array from port
     *
     * @param registersQuantity quantity of registers to be read
     * @return received byte array
     * @throws SerialPortException if an error occurred while reading data
     */
    private byte[] readBytes(int registersQuantity) throws SerialPortException, InterruptedException {
        byte[] buffer;
        buffer = SERIAL_PORT.readBytes(registersQuantity * 2 + 5);
        if (SERIAL_PORT.isOpened()) SERIAL_PORT.closePort();
        return buffer;
    }

    /**
     * Get data size
     *
     * @return data size in bytes
     * @deprecated
     */
    private int getDataSize(byte[] dataChunk) {
        final int INDEX_OF_DATA_SIZE = 2;
        return (int) dataChunk[INDEX_OF_DATA_SIZE];
    }

    /**
     * Convert two bytes integer to bytes array
     *
     * @param twoBytesInteger integer with two bytes
     * @return bytes array with two bytes
     * @throws IllegalArgumentException if param larger then two bytes
     */
    private byte[] twoBytesIntegerToByteArray(long twoBytesInteger) {
        byte firstByte = (byte) (twoBytesInteger >> 8);
        byte secondByte = (byte) twoBytesInteger;
        return new byte[]{firstByte, secondByte};
    }

    /**
     * Get bytes from output package
     *
     * @param outputPackage output package
     * @return byte array with output package
     * @deprecated
     */
    private byte[] packageToByteArray(OutputPackage outputPackage) {   // TODO optimize
        byte deviceAddress = (byte) outputPackage.getDeviceAddress();
        byte modbusFunction = (byte) outputPackage.getModbusFunction();
        byte[] registerAddress = twoBytesIntegerToByteArray(outputPackage.getRegisterAddress());
        byte[] dataSize = twoBytesIntegerToByteArray(outputPackage.getDataSize());
        byte[] crc = twoBytesIntegerToByteArray(outputPackage.getCRC());
        return new byte[]{deviceAddress, modbusFunction, registerAddress[0], registerAddress[1],
                dataSize[0], dataSize[1], crc[0], crc[1]};
    }

    /**
     * Calculate CRC of byte array
     *
     * @param data data for calculating
     * @return crc checksum
     */
    public int calculateCRC(byte[] data) {
        final int STAGES = 7;
        final int POLYNOMIAL = 0xA001;
        int crc = 0xFFFF;
        for (byte bt : data) {
            crc ^= bt;
            for (int i = 0; i <= STAGES; i++) {
                int beforeShift = crc & 1;
                crc = (crc >> 1);
                crc ^= (beforeShift == 1) ? POLYNOMIAL : 0x0;
            }
        }
        crc = (((byte) crc) << 8) + (byte) (crc >> 8);
        return crc;
    }

    /**
     * Check actual and expected CRC in package data
     *
     * @throws CRCException if actual and expected CRC aren't equals
     */
    private void checkCRC(InputPackage inputPackage) {
        int expectedCRC = inputPackage.getCRC();
        byte[] responseData = inputPackage.getResponseData();
        int actualCRC = calculateCRC(new byte[]{(byte) inputPackage.getDeviceAddress(),
                (byte) inputPackage.getModbusFunction(), (byte) inputPackage.getDataSize(),
                responseData[0], responseData[1], responseData[2], responseData[3]});
        /*if (actualCRC != expectedCRC) {
            throw new CRCException("Actual and expected CRC aren't equals.");
        }*/
    }

    /**
     * Convert four bytes integer to bytes array
     *
     * @param fourBytesInteger integer with four bytes
     * @return bytes array with four bytes
     * @throws IllegalArgumentException if param larger then four bytes
     */

    private byte[] fourBytesIntegerToByteArray(long fourBytesInteger) {
        byte firstByte = (byte) (fourBytesInteger >> 24);
        byte secondByte = (byte) (fourBytesInteger >> 16);
        byte thirdByte = (byte) (fourBytesInteger >> 8);
        byte fourthByte = (byte) fourBytesInteger;
        return new byte[]{firstByte, secondByte, thirdByte, fourthByte};
    }

    /**
     * Join two arrays
     *
     * @param first  first array of bytes
     * @param second second array of bytes
     * @return buffer result
     */
    public byte[] joinArrays(byte[] first, byte[] second) {
        if (first == null)
            return second;
        if (second == null)
            return first;
        byte[] buffer = new byte[first.length + second.length];

        for (int i = 0; i < first.length; i++)
            buffer[i] = first[i];
        for (int j = 0; j < second.length; j++)
            buffer[j + first.length] = second[j];
        return buffer;
    }

    /**
     * Send output package to port and return response data from input package
     *
     * @return response data
     * @throws SerialPortException if an error occurred while I/O to port
     */
    public byte[] getData() throws SerialPortException, InterruptedException, IOException {
        byte[] buffer;
        OutputPackage outputPackage = new RtuOutputPackage(
                deviceAddress,
                MODBUS_READ_INPUT_REGISTER_FUNCTION,
                1,
                46);
        InputPackage inputPackage;
        writePackage(outputPackage);
        Thread.sleep(500);
        inputPackage = new RtuInputPackage(readBytes(46));
        checkCRC(inputPackage);

        buffer = inputPackage.getResponseData();
        outputPackage = new RtuOutputPackage(
                deviceAddress,
                MODBUS_READ_INPUT_REGISTER_FUNCTION,
                47,
                DATA_SIZE - 46);
        writePackage(outputPackage);
        Thread.sleep(500);
        inputPackage = new RtuInputPackage(readBytes(DATA_SIZE - 46));
        checkCRC(inputPackage);

        buffer = joinArrays(buffer, inputPackage.getResponseData());
        return buffer;
    }
}
